/**
 * 面试题集锦
 */

// 题1：如何实现让 if (a == 1 && a == 2) 条件成立 重写 a 对象的 valueOf() 方法
// 解法一核心思想：== 比较时如果一个操作数是对象，另一个操作数不是，则调用对象的valueOf()方法/toString()方法将其转换成字符串
let a0 = {
  value: 0,
  // valueOf: function() {
  //   this.value++
  //   return this.value
  // },
  toString: function() {
    this.value++
    return this.value
  }
}
console.log('表达式 a0 == 1 && a0 == 2 的值为：' + (a0 == 1 && a0 == 2))

// 解法二核心思想：和解法一一样，利用特殊的对象->数组，重写他的valueOf()方法，设置为shift方法，删除数组第一个值并返回
let a1 = [1, 2, 3]
a1.valueOf = a1.shift
console.log('表达式 a1 == 1 && a1 == 2 && a1 == 3 的值为：' + (a1 == 1 && a1 == 2 && a1 == 3))

// 解法三核心思想：利用Object.defineProperty()方法
Object.defineProperty(window, 'a2', {
  get: function() {
    this.value ? this.value++ : this.value = 1
    return this.value
  }
})
console.log('表达式 a2 == 1 && a2 == 2 && a2 == 3 的值为：' + (a2 == 1 && a2 == 2 && a2 == 3))

// 题2：以下代码输出及为什么会输出此结果
for (var i = 0; i < 5; i ++) {
  setTimeout(function timer() {
    console.log(i)
  }, 0)
}
// 连续输出 5 次 5，因为 js 是单线程执行，循环体中内容是计时器，是异步的宏任务，它的回调会等到主线程同步任务全部执行完再执行，同步任务执行完，
// i 的值为 5，执行计时器中的回调打印 i，而当前计时器回调函数作用域中不包含变量 i，往上一级（window下面）找到了变量 i，5个回调全部打印出 i 的值

// 题3：怎么让题2的代码输出 0 1 2 3 4
// 方法一：使用IIFE(立即执行函数表达式) 把循环的变量i作为参数传递给立即执行函数表达式
for (var i = 0; i < 5; i ++) {
  (function(val) {
    setTimeout(function timer() {
      console.log(val)
    }, 0)
  })(i)
}

// 方法二：给定时器传入第三个参数，作为回调函数的第一个参数，值设置为循环变量i
for (var i = 0; i < 5; i ++) {
  setTimeout(function timer(val) {
    console.log(val)
  }, 0, i)
}

// 方法三：使用 es6 的 let 定义变量 i，var 会变量提升，会把定义变量提升到当前函数作用域或者全局作用域，let是块级作用域，不会变量提升
// 使用 let 相当于每个回调函数和 i 都在同一个块级作用域，拿到的就是变量 i 循环时对应的值
for (let i = 0; i < 5; i ++) {
  setTimeout(function timer() {
    console.log(i)
  }, 0)
}

// 类数组，包含callee、length和迭代器([Symbol.iterator])属性，下标从0开始
// 常见的类数组有 arguments对象、getElementsByTagName/getElementsByClassName获取的HtmlCollection、querySelector获取的NodeList
// 题3：类数组转数组的方法有以下五种
function arrLikeToArr() {
  const inputEles = document.getElementsByTagName('input')
  console.log(inputEles)
  let arr1 = Array.prototype.slice.call(inputEles)
  console.log('1、slice.call转换：', arr1)
  let arr2 = Array.prototype.concat.apply([], inputEles)
  console.log('2、concat.apply转换：', arr2)
  let arr3 = Array.from(inputEles)
  console.log('3、Array.from转换：', arr3)
  let arr4 = [...inputEles]
  console.log('4、ES6展开运算符转换：', arr4)
  let arr5 = []
  for (let i = 0; i < inputEles.length; i++) {
    arr5.push(inputEles[i])
  }
  console.log('5、for循环添加到新数组转换：', arr5)
}

// 题4：判断数组是否包含某个值
// arr.indexOf(value) !== -1
// arr.includes(value, 0)
// arr.find(item => item === value)
// arr.findIndex(item => item === value) !== -1
function contains(arr, value) {
  return arr.some(item => item === value)
}

/**
 * 题5：数组扁平化flat => 多维数组变一维数组(原型上实现请参考 Array.property.flat)
 * 1、ES6的arr.flat(Infinity) 展开任意深度的嵌套数组
 * 2、JSON.stringify + replace + JSON.parse：JSON.parse(`[${JSON.stringify(arr).replaceAll('[', '').replaceAll(']', '')}]`)
 * 3、实现拉平一层
 * 3.1 concat + apply：[].concat.apply([], arr)
 * 3.2 concat + 扩展运算符：[].concat(...arr)
 * 3.3 reduce: arr.reduce((acc, cur) => acc.concat(cur), [])
 * 4. reduce + concat + isArray + 循环实现(reduce会忽略数组空位，slice不会忽略空位)
 */
function reduceFlat(arr = [], depth = 1) {
  // 注意depth为0时arr.slice()不会忽略空位，使用forEach循环可解决此问题
  let result = []
  if (depth > 0) {
    result = arr.reduce((acc, cur) => acc.concat(Array.isArray(cur) ? reduceFlat(cur, depth - 1) : cur), [])
  } else {
    // result = arr.slice()
    arr.forEach(item => result.push(item))
  }
  return result
}

/**
 * 5.forEach + isArray + push + 循环实现
 */
function forEachFlat(arr = [], depth = 1) {
  let result = []
  // 如果展开的深度不是数值或者小于0的负数，默认去空
  if (typeof depth !== 'number' || typeof depth === 'number' && depth < 0) {
    arr.forEach(item => result.push(item))
  } else {
    // 利用 IIFE 立即执行函数表达式开始递归
    (function flat(arr, depth) {
      // forEach 会自动去除数组空位
      arr.forEach((item) => {
        // 控制递归深度
        if (Array.isArray(item) && depth > 0) {
          // 递归数组
          flat(item, depth - 1)
        } else {
          // 缓存元素
          result.push(item)
        }
      })
    })(arr, depth)
  }
  // 返回递归结果
  return result
}

/**
 * 6.for of + isArray + push + 循环实现
 * for of 循环不会去除空位，需要手动去除 void 0 === undefined
 */
function forFlat(arr = [], depth = 1) {
  let result = []
  // 如果展开的深度不是数值或者小于0的负数，默认去空
  if (typeof depth !== 'number' || typeof depth === 'number' && depth < 0) {
    arr.forEach(item => result.push(item))
  } else {
    // 利用 IIFE 立即执行函数表达式开始递归
    (function flat(arr, depth) {
      for (let item of arr) {
        if (Array.isArray(item) && depth > 0) {
          // 递归数组
          flat(item, depth - 1)
        } else {
          // 非 undefined 缓存元素
          item !== void 0 && result.push(item)
        }
      }
    })(arr, depth)
  }
  // 返回递归结果
  return result
}

/**
 * 7.堆栈Stack实现数组扁平化，不加深度控制
 * 注意：深度的控制比较低效，因为需要检查每一个值的深度。可能在 shift / unshift 上进行 w/o 反转，但是末端的数组 OPs 更快
 */
function stackflat(arr = []) {
  // 1.先使用扩展运算符解构数组得到stack
  const stack = [...arr]
  // 定义缓存数组
  const res = []
  // 2.判断栈中是否还有数据
  while (stack.length) {
    console.log(stack)
    // 3.栈中元素出栈
    const item = stack.pop()
    // 4.1判断当前元素是否为数组
    if (Array.isArray(item)) {
      // 5.把当前元素使用扩展运算符再次入栈
      stack.push(...item)
    } else {
      // 4.2直接把获取到的元素存入缓存数组(此时手动去空)
      item !== void 0 && res.push(item)
    }
  }
  // 6.返回结果，此时需要翻转栈的结果
  return res.reverse()
}

/**
 * 8.利用生成器 Generator 实现
 * 生成器转数组的使用方法 [...generatorflat(arr)]
 */
function* generatorflat(arr = []) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* generatorflat(item)
    } else {
      // yield item
      item !== void 0 ? yield item : null
    }
  }
}

// 题6：判断元素是否为数组
// 1. instanceof 操作符对于多种全局环境不适应，windows下视为正常（感觉无需了解太深）
// return ele instanceof Array
// 2. constructor属性是可以修改的，比如字符串的constructor属性也可以改为Array
// return ele.constructor === Array
// 3. 精准判断类型，还能精准判断Function、Math、Date等对象
// return Object.prototype.toString.call(ele) === '[object Array]'
// 4. 数组原生的判定方法，通常此方法判断
function isArray(ele) {
  return Array.isArray(ele)
}

/**
 * 题7：对于某种特定格式的多维数组，children属性是数组，并且这个数组中又还有children属性，查找某个特定的属性的项或者另一个属性的值
 * @param {Array} array 给定的数组
 * @param {String} srcKey 查找的源属性key 比如查找 id = 1 的项中 name 的值，srcKey = 'id'、srcVal = 1、targetKey = 'name'
 * @param {*} srcVal 查找的源属性value 
 * @param {String} recKey 递归的属性名称，并且这个属性必须是数组，比如 children
 * @param {String} targetKey 需要查找的目标属性key
 * 重点：1.返回值的判断
 * 2.不使用 forEach 循环，因为 forEach 循环中 return 不能终止方法，只是终止了 forEach 第一个参数 callback 的回调
 */
/** 
 * 3.示例数据结构：list = [{
 *    children: [{
 *      id: 1,
 *      name: '李四'
 *    }, {
 *      children: [{
 *        id: 4,
 *        name: '码牛逼'
 *      },
 *      id: 3,
 *      name: '王老虎'
 *    ]
 *  }, {
 *    id: 0,
 *    name: '张三',
 *    children: []
 *  }]
 */
function filterArrayData(array, srcKey, srcVal, recKey, targetKey) {
  // for循环实现
  for (let i = 0; i < array.length; i++) {
    // 1.找到直接返回需要查找的目标属性值，如果返回目标项，直接修改为 array[i]
    if (array[i][srcKey] === srcVal) {
      return array[i][targetKey]
    }
    // 2.当前对象未找到符合条件，先深度遍历，递归他的子数组
    if (array[i][recKey] && array[i][recKey].length) {
      const targetVal = filterArrayData(array[i][recKey], srcKey, srcVal, recKey, targetKey)
      // 3.重点：此处需要判断找到才 return 终止循环，没找到不处理，函数会自动在当前层次的上一层继续查找，
      // 直到最外层也找不到，此函数不提供返回值，相当于返回 undefined
      if (targetVal) return targetVal

      // 4.以下为错误示范，没做判断，导致第一层到最底层没找到就直接终止了循环，就会丢数据
      // return filterArrayData(array[i][recKey], srcKey, srcVal, recKey, targetKey)
    }
  }
  // 5.for of 实现步骤一样，只是 for (const item of array) {} item <=> array[i]
}

/**
 * 题8：this 指向问题
 * 1) new 调用 => this 指向调用的构造函数
 * 2) 使用 bind、call、apply 调用 => 传 null 或者 undefined，指向 window，否则指向传入的函数
 * 3) 对象调用 => 指向调用方法的对象
 * 4) 默认绑定 => 全局上下文、直接调用、计时器、IIFE 都指向 window
 * 5) 箭头函数 => 没有 this，不能绑定，里面的 this 会指向当前最近的非箭头函数的 this，找不到就指向 window(严格模式下是 undefined)
 * 6) DOM 事件绑定 => onclick 和 addEventListener 中 this 指向事件绑定的元素，IE 的 attachEvent 绑定事件 this 指向 window
 */

/**
 * 题9：浅拷贝 只能拷贝一层对象，不能拷贝嵌套的对象，嵌套的对象只拷贝其引用（改变源对象中该引用中的值，拷贝对象该值也改变）
 * 浅拷贝方法
 * 1) Object.assign(targetObj, ...srcObj) 源对象中的属性会覆盖目标对象中的属性，只拷贝源对象自身并且可枚举的属性到目标对象
 * 2) concat() 浅拷贝数组 arr.concat() / [].concat(...arr) / [].concat.apply([], arr)
 * 3) slice() 浅拷贝数组 arr.slice() / Array.prototype.slice.call(arr)
 * 4) ...展开运算符 浅拷贝对象或者数组 { ...obj } [ ...arr ]
 */
function shallowClone (target) {
  // 先操作符判断目标参数是否为对象
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    // 注意：数组会跳过空值，但是最终结果还是会有空值，它只在对应位置去设置值
    for (let prop in target) {
      console.log(prop);
      // 只克隆自身属性
      if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}
/**
 * 题9-附加：给数组去除空值
 */
function delEmpty (arr) {
  let O = Object(arr);
  let len = arr.length >>> 0;
  let result = [];
  let resLen = 0;
  for (let k = 0; k < len; k++) {
    if (k in O) {
      result[resLen++] = O[k];
    }
  }
  return result;
}

/**
 * 题10：深拷贝
 * 1) 简易版深拷贝：JSON.parse(JSON.stringify())
 * 存在的问题1：循环引用的问题 => 使用 WeakMap 弱引用解决循环引用及对象无法被回收问题
 * 存在的问题2：无法拷贝特殊对象 => 使用 toString 区分对象、RegExp、Date、Set、Map等对象
 * 存在的问题3：无法拷贝函数 => prototype属性区分普通函数与箭头函数，箭头函数没有原型对象。正则获取函数参数与函数体，使用new Function创建函数对象
 */

/**
 * 题11：节流 核心思想：如果在定时器的时间范围内再次触发，则不予理睬，等当前定时器完成，才能启动下一个定时器任务。
 * 即：给定时间内方法只会触发第一次，经过给定时间后才能触发下一次。以下两种写法，第二种更好理解
 * 举例：快速点击表单提交按钮，只在给定时间内触发一次请求
 */
function throttle(fn, interval) {
  let flag = true;
  return function(...args) {
    if (!flag) return ;
    let context = this;
    flag = false;
    setTimeout(() => {
      fn.apply(context, args);
      // 每次调用方法之后都把标志设为 true
      flag = true;
    }, interval);
  };
}
function throttle2(fn, interval) {
  let last = 0;
  return function(...args) {
    let now = +new Date();
    // 当前时间减去上次调用时间小于给定时间，不触发
    if (now - last < interval) return ;
    last = now;
    fn.apply(this, args);
  };
}

/**
 * 题12：防抖 核心思想：每次事件触发则删除原来的定时器，建立新的定时器，从最后一次触发重新开始计时。
 * 举例：王者荣耀回城类似，反复点击回城，回城时间以最后一次触发开始计时。
 * 缺陷：短时间内一直频繁触发，会导致一次响应都没有
 */
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    let context = this;
    // 每次触发都清空定时器
    if (timer) clearTimeout(timer);
    timer = setTimeout(function() {
      fn.apply(context, args)
    }, delay)
  }
}

/**
 * 题13：加强版节流：防抖有时候触发的太频繁会导致一次响应都没有，我们希望到了固定的时间必须给用户一个响应
 */
function strongThrottle(fn, delay) {
  let last = 0,
  timer = null;
  return function(...args) {
    let context = this;
    let now = +new Date();
    // 时间没到
    if (now - last < delay) {
      clearTimeout(timer);
      setTimeout(function() {
        last = now;
        fn.apply(context, args);
      }, delay);
    } else {
      // 时间到了，必须触发一次
      last = now;
      fn.apply(context, args);
    }
  }
}

/**
 * 题14：实现图片懒加载原理：通过监听 scroll 事件来判断图片是否到达视口（浏览器可视区域 document.documentElement.clientHeight）
 * 注意：获取元素原生的属性如 src 直接通过 .src 获取即可，自定义属性使用 .getAttribute('data-src')
 */
// 方案一 使用 offsetTop < scrollTop + viewHeight 判断图片是否出现在视口
// 方案二 判断图片出现在视口，使用 getBoundingClientRect API 
// if (imgEles[i].getBoundingClientRect().top < viewHeight) 替换方案一中的 425 行的判断条件
function lazyload() {
  // 1.获取视口高度
  let viewHeight = document.documentElement.clientHeight;
  // 2.获取滚动条上下滚动的高度
  let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  // 3.获取图片元素
  let imgEles = document.getElementsByTagName('img');
  // 4.遍历判断当前图片是否出现在视口中，出现的话就把图片的 src 属性修改成自定义属性的值
  for (let i = 0; i < imgEles.length; i++) {
    // 5.图片距离顶部的距离 < 当前滚动条距顶部的距离 + 视口的高度 => 当前图片已经进入了视口
    if (imgEles[i].offsetTop < scrollTop + viewHeight) {
      // 6.当前图片已经设置了非默认值的 src 属性，跳过当前图片的修改
      if (imgEles[i].src !== 'default.jpg') continue ;
      // 7.设定默认值的图片则修改其 src 属性为自定义属性 data-src 的值
      imgEles[i].src = imgEles[i].getAttribute('data-src');
    }
  }
}

// 方案三 使用浏览器内置的 API IntersectionObserver 
// 该 API 实现了监听 window 的 scroll 事件、判断是否出现在视口、节流三大功能
function lazyload2() {
  // 1.获取图片元素
  let imgEles = document.getElementsByTagName('img');
  const observer = new IntersectionObserver(watchers => {
    // watchers 指向被观察的元素
    for (let i = 0; i < watchers.length; i++) {
      // 判断当前被观察元素是否出现在视口
      if (watchers[i].isIntersecting) {
        const imgEle = watchers[i].target;
        imgEle.src = imgEle.getAttribute('data-src');
        // 去除对该元素的观察
        observer.unobserve(imgEle);
      }
    }
  })
  // 把获取的 img 的所有元素节点类数组 HtmlCollection 转换成数组之后遍历给每个 img 元素使用 observer 对象进行观察
  Array.from(imgEles).forEach(item => observer.observe(item));
}